package pt.compflow.tools;

import java.io.InputStream;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.hp.hpl.jena.ontology.OntClass;
import com.hp.hpl.jena.ontology.OntModel;
import com.hp.hpl.jena.rdf.model.Model;
import com.hp.hpl.jena.rdf.model.ModelFactory;
import com.hp.hpl.jena.rdf.model.RDFList;
import com.hp.hpl.jena.rdf.model.RDFNode;
import com.hp.hpl.jena.rdf.model.Resource;
import com.hp.hpl.jena.rdf.model.Statement;
import com.hp.hpl.jena.rdf.model.StmtIterator;
import com.hp.hpl.jena.util.iterator.ExtendedIterator;
import com.hp.hpl.jena.vocabulary.RDF;

/**
 * ClassMapManager Maps a set of OntClasses to a Java class.
 * The underlying subClassOf relationships between OntClasses are used
 * so that a Java class mapped to a top-level class is also mapped to
 * all its sub-classes, unless a sub-class specific map exists. 
 * 
 * @param <T> T is the Java top-level interface; all registered Java classes
 * must implement this interface
 */
public class ClassMapManager<T extends Copyable<T>> {
	
	private static final Logger LOG = LoggerFactory.getLogger(ClassMapManager.class);
	
	private MessageDigest md5;
	private Map<String, T> map;
	
	public ClassMapManager() {
		map = new HashMap<String, T>();
		try {
			md5 = MessageDigest.getInstance("MD5");
		} catch (NoSuchAlgorithmException e) {
			LOG.warn("Error instantiating MD5 algorithm. Manager keys will take much more memory space!", e);
		}
	}
	
	private boolean areValidOntClasses(Resource... ontClasses) {
		for(Resource oc : ontClasses)
			if(oc == null) return false;
		return true;
	}
	
	/**
	 * Gets the MD5 hash of the concatenation of all OntClass URIs
	 * The MD5 hash will be unique for each set of ordered OntClasses; unless
	 * the set contains the same OntClasses in the exact same order
	 * @param ontClasses
	 * @return MD5 hash; URI if only one OntClass is passed; 
	 * 	concatenated URIs if no MD5 algorithm is found
	 */
	private String getKeyForOntClasses(Resource... ontClasses) {
		
		// in case there's only one ontClass, just return the URI (better for debugging)
		if(ontClasses.length == 1) return ontClasses[0].getURI();
		
		// get the concat string of all ontclass URIs
		StringBuilder builder = new StringBuilder();
		for(Resource oc : ontClasses)
			builder.append(oc.getURI()+";");
				
		// in case MD5 was not found
		if(md5 == null)
			return builder.toString();
		
		// initialize MD5 algorithm with the concat string
		// get the MD5 hash from concat string
		byte[] digest = md5.digest(builder.toString().getBytes());
		
		BigInteger bigInt = new BigInteger(1, digest);
		String hashtext = bigInt.toString(16);
		
		// now we need to zero pad it if you actually want the full 32 chars
		while(hashtext.length() < 32)
			hashtext = "0" + hashtext;
		
		return hashtext;
	}
	
	/**
	 * Registers a mapping between the given java class path/name and the given OntClasses
	 * 
	 * @param javaClassPath
	 * @param ontClasses
	 * @throws ClassNotFoundException
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 * @throws NoSuchAlgorithmException
	 */
	@SuppressWarnings("unchecked")
	public void register(String javaClassPath, Resource... ontClasses) throws ClassNotFoundException, InstantiationException, IllegalAccessException, NoSuchAlgorithmException {
		if(javaClassPath == null || !areValidOntClasses(ontClasses)) return;		
		Class<? extends T> clazz = (Class<? extends T>) Class.forName(javaClassPath);
		register(clazz, ontClasses);
	}
	
	public void register(Class<? extends T> javaClass, Resource... ontClasses) throws InstantiationException, IllegalAccessException, NoSuchAlgorithmException {
		if(javaClass == null || !areValidOntClasses(ontClasses)) return;			
		T refInstance = javaClass.newInstance();
		map.put(getKeyForOntClasses(ontClasses), refInstance);	
	}
	
	/**
	 * Unregisters a mapping for the given OntClasses
	 * 
	 * @param ontClasses
	 * @return
	 * @throws NoSuchAlgorithmException
	 */
	public boolean unregister(Resource... ontClasses) throws NoSuchAlgorithmException {
		if(!areValidOntClasses(ontClasses)) return false;
		return map.remove(getKeyForOntClasses(ontClasses)) != null;
	}
	
	/**
	 * Recursively checks for mappings for superClasses of the ontClass
	 *  
	 * @param model - ontology model
	 * @param ontClass - the class to check for a mapping
	 * @return instance of the mapped java class, if found, or null if not found
	 */
	private T getRec(OntModel model, OntClass ontClass) {
				
		// find the superclasses of ontClass
		ExtendedIterator<OntClass> superClassIt = ontClass.listSuperClasses(true);
		while(superClassIt.hasNext()) {
			OntClass superClass = superClassIt.next();
			
			// consider only URI classes (named classes)
			if(superClass.isURIResource()) {
				String key = getKeyForOntClasses(superClass);
				if(map.containsKey(key))
					// return a new instance if the mapping is found
					return map.get(key).copy();	
				else {
					// otherwise continue searching through superclasses
					T tmp = getRec(model, superClass);
					if(tmp != null) return tmp;
				}
			}
		}
				
		return null;
	}

	/**
	 * Get a mapped Java class instance for the supplied OntClasses, if found
	 * If a direct mapping for the set of supplied classes is found, a instance for
	 * this mapping is returned
	 * Otherwise, an instance for the mapping of the first mapped parent class of the 
	 * first supplied class (ontClasses[0]) is returned
	 * 
	 * @param ontClasses - set of OntClasses
	 * @return instance of the mapped java class, if found, or null if not found
	 */
	public T get(OntModel ontModel, Resource... ontClasses) {

		if(!areValidOntClasses(ontClasses)) return null;
		
		// if a direct class is found return...
		String key = getKeyForOntClasses(ontClasses);
		if(map.containsKey(key))
			return map.get(key).copy();
		
		if(ontModel == null) {
			LOG.warn("Cannot check parent class mappings since the OntModel is NULL!");
			return null;
		}
		
		// parent class finding is only available with a single ontclass
		if(ontClasses.length > 1) return null;
		
		// otherwise search with superclasses, recursively
		return getRec(ontModel, ontModel.getOntClass(ontClasses[0].getURI()));
	}
	
	/**
	 * Loads class mappings from an N3 file according to the http://compflow.pt/config vocabulary.
	 * TODO create documentation for the http://compflow.pt/config vocabulary
	 * 
	 * @param f - the N3 file
	 * @throws IllegalAccessException 
	 * @throws InstantiationException 
	 * @throws ClassNotFoundException 
	 * @throws NoSuchAlgorithmException 
	 */
	public void loadFromN3(InputStream is) throws NoSuchAlgorithmException, ClassNotFoundException, InstantiationException, IllegalAccessException {
				
		// this is a good example of how confusing it can be to read data with the base model class of Jena
		// you have to think in terms of a graph with statements (triples), properties, nodes, etc. 
		
		// 1. load the N3 configuration file into a default Jena Model
		Model m = ModelFactory.createDefaultModel();
		m.read(is, null, "N3");
		
		// 2. load all mappings in the configuration file
		// list all statements/triples with predicate RDF.type and object #ClassMap
		// i.e., list all subjects of type #ClassMap (something like a "listClassMaps" if a specific model existed)
		StmtIterator stmtIt = m.listStatements((Resource) null, RDF.type,  m.getResource("http://compflow.pt/config#ClassMap"));
		while(stmtIt.hasNext()) {
			Statement stm = stmtIt.next();
			
			// get the actual subject of type #ClassMap
			Resource map = stm.getSubject();
			// get the code value (the java class location and name) associated with this subject (map)
			RDFNode javaClassRes = map.getProperty(m.getProperty("http://compflow.pt/config#code")).getObject();
			// get the ontClasses value associated with this subject (map)
			Resource ontClassesRes = map.getPropertyResourceValue(m.getProperty("http://compflow.pt/config#ontClasses"));
			
			// the ontClasses should be a RDFList
			if(!javaClassRes.isLiteral() || !ontClassesRes.canAs(RDFList.class)) 
				throw new RuntimeException("Invalid configuration file");
			
			String javaClassPath = javaClassRes.asLiteral().getString();
			RDFList ontClassesList = ontClassesRes.as(RDFList.class);
			List<RDFNode> list = ontClassesList.asJavaList();
			
			// turn the list into the type received by register()
			Resource[] ontClasses = new Resource[list.size()];
			for(int i=0;i<list.size();i++) {
				ontClasses[i] = list.get(i).asResource();
			}
			
			// register the mapping
			register(javaClassPath, ontClasses);
		}

	}
	
}
